﻿using System;
using System.Text;
using System.Drawing;
using System.Drawing.Imaging;

namespace PhotoManager
{
    /// <summary>
    /// Contains information about the histogram of a color channel.
    /// </summary>
    public class ColorDistribution
    {
        private readonly int[] m_histogram;
        private readonly int m_pixelCount;

        private ColorDistribution(int[] histogram)
        {
            m_histogram = histogram;
            m_pixelCount = 0;

            if (histogram != null)
            {
                for (int i = 0; i < histogram.Length; i++)
                    m_pixelCount += histogram[i];
            }
        }

        public int PixelCount
        {
            get { return m_pixelCount; }
        }

        public static ColorDistribution Compute(string fileName, int binCount)
        {
            using (Image image = Bitmap.FromFile(fileName))
            {
                return ColorDistribution.Compute((Bitmap)image, binCount);
            }
        }

        public static ColorDistribution Compute(Bitmap image, int bucketCount)
        {
            if (image == null)
                throw new ArgumentNullException("image");
            if (bucketCount <= 0)
                throw new ArgumentException("bucketCount must be greater than zero.");

            int[] bucketPixels = new int[bucketCount];

            // Convert the image to RGB format.
            Rectangle rect = new Rectangle(0, 0, image.Width, image.Height);
            using (Bitmap newImage = image.Clone(rect, PixelFormat.Format24bppRgb))
            {
                BitmapData bitmapData = newImage.LockBits(rect, ImageLockMode.ReadOnly, PixelFormat.Format24bppRgb);
                IntPtr ptr = bitmapData.Scan0;
                byte[] scanline = new byte[bitmapData.Width * 3];
                for (int i = 0; i < bitmapData.Height; i++)
                {
                    System.Runtime.InteropServices.Marshal.Copy(ptr, scanline, 0, scanline.Length);

                    // Compute brightness value
                    // http://stackoverflow.com/questions/596216/formula-to-determine-brightness-of-rgb-color
                    for (int j = 0; j < scanline.Length; j += 3)
                    {
                        byte r = scanline[j];
                        byte g = scanline[j + 1];
                        byte b = scanline[j + 2];
                        double brightness = (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255;
                        int bucket = (int)Math.Round(brightness * (bucketCount - 1));
                        bucketPixels[bucket]++;
                    }
                    // Move to next scanline
                    ptr = new IntPtr(ptr.ToInt64() + bitmapData.Stride);
                }
                newImage.UnlockBits(bitmapData);
            }

            // Normalize the buckets.
            ColorDistribution distribution = new ColorDistribution(bucketPixels);
            return distribution;
        }

        public static ColorDistribution Parse(string s)
        {
            if (s == null)
                throw new ArgumentNullException("s");

            string[] freq = s.Split(',');
            int[] histogram = new int[freq.Length];
            for (int i = 0; i < freq.Length; i++)
                histogram[i] = int.Parse(freq[i]);

            return new ColorDistribution(histogram);
        }

        /// <summary>
        /// Returns a comma-separated string of the frequencies.
        /// </summary>
        /// <returns></returns>
        public override string ToString()
        {
            StringBuilder sb = new StringBuilder();
            for (int i = 0; i < m_histogram.Length; i++)
            {
                if (i > 0)
                    sb.Append(',');
                sb.Append(m_histogram[i]);
            }
            return sb.ToString();
        }

        // http://en.wikipedia.org/wiki/Bhattacharyya_distance
        public static double GetSimilarity(ColorDistribution d1, ColorDistribution d2)
        {
            if (d1 == null)
                throw new ArgumentNullException("d1");
            if (d2 == null)
                throw new ArgumentNullException("d2");
            if (d1.m_histogram.Length != d2.m_histogram.Length)
                throw new ArgumentException("Distributions must have same number of partitions.");

            int count1 = d1.PixelCount;
            int count2 = d2.PixelCount;

            double similarity = 0;
            for (int i = 0; i < d1.m_histogram.Length; i++)
            {
                similarity += Math.Sqrt((double)d1.m_histogram[i] * (double)d2.m_histogram[i]);
            }
            similarity /= Math.Sqrt((double)count1 * (double)count2);
            return Math.Min(similarity, 1.0);
        }

        public static double GetDistance(ColorDistribution d1, ColorDistribution d2)
        {
            return Math.Sqrt(1.0 - GetSimilarity(d1, d2));
        }

        public static double GetDistance(ColorDistribution d1, ColorDistribution d2, HistogramDistanceType type)
        {
            if (type == HistogramDistanceType.Simple)
                return GetSimpleDistance(d1, d2);
            else if (type == HistogramDistanceType.Bhattacharyya)
                return GetDistance(d1, d2);
            else
                throw new ArgumentException();
        }

        public static double GetSimpleDistance(ColorDistribution d1, ColorDistribution d2)
        {
            if (d1 == null)
                throw new ArgumentNullException("d1");
            if (d2 == null)
                throw new ArgumentNullException("d2");
            if (d1.m_histogram.Length != d2.m_histogram.Length)
                throw new ArgumentException("Histograms must have same number of bins.");

            double d = 0.0;
            int numBins = d1.m_histogram.Length;
            for (int i = 0; i < numBins; i++)
            {
                d += Math.Abs((double)d1.m_histogram[i] / d1.PixelCount
                    - (double)d2.m_histogram[i] / d2.PixelCount);
            }
            return d;
        }
    }

    public enum HistogramDistanceType
    {
        Simple = 0,
        Bhattacharyya = 1,
    }
}
